﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net.Mail;
using System.Resources;
using System.Threading;
using System.Net.Mime;

namespace ExchangeHog.MessageParser
{
	public class ParserException: Exception
	{
		public ParserException(string message): base(message) {}
	}

	public class NewParser
	{
		ResourceManager rm;

		public NewParser()
		{
			rm = new ResourceManager(typeof(Lexer));
		}
		private string GetMessage(string resourcename, params object[] args) { return String.Format(rm.GetString(resourcename, Thread.CurrentThread.CurrentCulture), args); }

		//
		//	address = mailbox / group
		//	address = 
		//		addr-spec / 
		//		( phrase route-addr ) / 
		//		group
		//
		//	phrase = ( encoded-word / word )+
		//
		//	address = 
		//		( local-part "@" domain ) / 
		//		( ( encoded-word / word )+ "<" [route] local-part "@" domain ">" ) / 
		//		( encoded-word / word )+ ":" [#mailbox] ";"
		//	route       =  1#("@" domain) ":" 
		//  local-part  =  (word *("." word))             ; uninterpreted case-preserved
		//	domain = sub-domain *("." sub-domain)
		//	sub-domain  =  (domain-ref / domain-literal)
		//	domain-literal =  "[" *(dtext / quoted-pair) "]"
		//	domain-ref  =  atom                         ; symbolic reference
		//	domain = (atom / ( "[" *(dtext / quoted-pair) "]") ) *("." (atom / "[" *(dtext / quoted-pair) "]"))
		//	address = 
		//		( (word *("." word)) "@" (atom / "[" *(dtext / quoted-pair) "]") *("." (atom / "[" *(dtext / quoted-pair) "]")) ) / 
		//		( ( encoded-word / word )+ "<" [1#("@" (atom / "[" *(dtext / quoted-pair) "]") *("." (atom / "[" *(dtext / quoted-pair) "]"))) ":"] (word *("." word)) "@" (atom / "[" *(dtext / quoted-pair) "]") *("." (atom / "[" *(dtext / quoted-pair) "]")) ">" ) / 
		//		( encoded-word / word )+ ":" [#(( (word *("." word)) "@" (atom / "[" *(dtext / quoted-pair) "]") *("." (atom / "[" *(dtext / quoted-pair) "]")) ) / ( ( encoded-word / word )+ "<" [route] (word *("." word)) "@" (atom / "[" *(dtext / quoted-pair) "]") *("." (atom / "[" *(dtext / quoted-pair) "]")) ">" ))] ";"
		//
		//	address =
		//		(...) ("@" | "<" | ":") (...)
		//
		//	domain-literal = "[" *(dtext / quoted-pair) "]"
		//	domain = (atom | domain-literal) ("." (atom | domain-literal))*
		//	route = ("@" domain)+ ":"
		//	address =
		//		(...)	(	"@" domain |
		//					"<" [route] word ("." word)* "@" domain ">" |
		//					":" (mailbox ("," mailbox)*)? ";" )
		//	
		//	mailbox = addr-spec | ( phrase route-addr)
		//	mailbox = (...) ("@" | "<") (...)
		//
		//	word-sequence = ( {1} (word *("." word)) | {2} (word | encoded-word)+ )
		//	mailbox = word-sequence ({1} "@" | {2} "<") (...)
		//
		//	mailbox-pre =
		//		word ("." word)+  "@" |
		//		word "@" |
		//		word "<" |
		//		word (word | encoded-word)+ "<" |
		//		encoded-word (word | encoded-word)* "<"
		//
		//	mailbox-pre =
		//		word ( 
		//			("." word) + "@" |
		//			"@" |
		//			"<" |
		//			(word | encoded-word)+ "<"
		//		) |
		//		encoded-word (word | encoded-word)* "<"
		//
		const int SIMPLE_ADDR = 0x01;
		const int SIMPLE_ADDR_WITH_NAME = 0x02;
		const int GROUP = 0x04;
		struct PreAddressWords
		{
			public enum PreAddressType { NONE, SIMPLE_ADDR, SIMPLE_ADDR_WITH_NAME, GROUP };
			PreAddressType pat;

			IList<string> words;
		}
		private PreAddressWords ParsePreAddressWords(Lexer l, bool isGroupAllowed)
		{
			int allowedAddr;

			Token t = l.NextToken(Lexer.ContextType.CONTEXT_ADDRESS_HEADER_VALUE);
			switch (t.TType)
			{
				case Token.TokenType.CAHV_ENCODED_WORD: // either (phrase route-addr) or group
					allowedAddr = SIMPLE_ADDR_WITH_NAME | GROUP;
					break;
				case Token.TokenType.CAHV_WORD:
					allowedAddr = SIMPLE_ADDR | SIMPLE_ADDR_WITH_NAME | GROUP;
					break;
				default:
					throw new ParserException(GetMessage("ParserError_UnexpectedToken", t.TType));
			}
			t = l.NextToken();
			if (t.TType == Token.TokenType.CAHV_PERIOD) // only simple addr
			{
				if ((allowedAddr & SIMPLE_ADDR) != SIMPLE_ADDR)
					throw new ParserException(GetMessage("ParserError_UnexpectedToken", t.TType));

				do
				{
					t = l.NextToken();
					if (t.TType != Token.TokenType.CAHV_WORD)
						throw new ParserException(GetMessage("ParserError_UnexpectedToken", t.TType));
					t = l.NextToken();
				} while (t.TType == Token.TokenType.CAHV_PERIOD);

				if (t.TType != Token.TokenType.CAHV_AT)
					throw new ParserException(GetMessage("ParserError_UnexpectedToken", t.TType));
			}
			else
			{
				while (t.TType == Token.TokenType.CAHV_WORD || t.TType == Token.TokenType.CAHV_ENCODED_WORD)
					t = l.NextToken();
			}
			PreAddressWords.PreAddressType pat = PreAddressWords.PreAddressType.NONE;
			switch (t.TType)
			{
				case Token.TokenType.CAHV_LEFT_ANGLE_BRACKET:

				case Token.TokenType.CAHV_COLON:
					break;
				case Token.TokenType.CAHV_AT:
					if (!isGroupAllowed)
						throw new ParserException(GetMessage("ParserError_GroupNotAllowedHere", t.TType));
					ParseAddress1(l, false);
					break;
			}

		}


		//	address = 
		//		( word *("." word) "@" sub-domain *("." sub-domain) / 
		//		( word+ "<" [1#("@" domain ) ":"] word *("." word) "@" sub-domain *("." sub-domain) ">" ) /
		//		( word+ ":" [# mailbox ] ";" )
		//	address = 
		//		( word *("." word) "@" (atom / domain-literal) *("." (atom / domain-literal)) / 
		//		( word+ "<" [1#("@" domain ) ":"] word *("." word) "@" (atom / domain-literal) *("." (atom / domain-literal)) ">" ) /
		//		( word+ ":" [# mailbox ] ";" )
		//	word = atom / quoted-string
		//	address = 
		//		( word *("." word) "@" (atom / domain-literal) *("." (atom / domain-literal)) / 
		//		( word+ "<" routing-information ">" ) /
		//		( word+ ":" ... ";" )
		//
		private ICollection<String> ParseAddress(Lexer l)
		{
			List<String> addresses = new List<string>();

			StringBuilder sb = new StringBuilder();
			Token t = l.AssertNextTokenInContextIs(Lexer.ContextType.CONTEXT_ADDRESS_HEADER_VALUE, Token.TokenType.CAHV_ATOM, Token.TokenType.CAHV_QUOTED_STRING);
			int nWords = 0;
			do
			{
				nWords++;
				sb.Append(t.Repr);

				t = l.NextToken();
			}
			while (t.TType == Token.TokenType.CAHV_ATOM || t.TType == Token.TokenType.CAHV_QUOTED_STRING);

			switch (t.TType)
			{
				case Token.TokenType.CAHV_PERIOD:
					if (nWords > 1)
						throw new ParserException(GetMessage("ParserError_MoreThanOneWordBeforePeriod", t));
					do 
					{
						sb.Append(t.Repr);
						sb.Append(l.AssertNextTokenIs(Token.TokenType.CAHV_ATOM, Token.TokenType.CAHV_QUOTED_STRING));
						t = l.NextToken();
					} while (t.TType == Token.TokenType.CAHV_PERIOD);
					l.AssertThisTokenIs(t, Token.TokenType.CAHV_AT);
					sb.Append(t.Repr);
					sb.Append(l.AssertNextTokenIs(Token.TokenType.CAHV_ATOM, Token.TokenType.CAHV_DOMAIN_LITERAL).Repr);
					t = l.NextToken();
					if (t.TType == Token.TokenType.CAHV_PERIOD)
						do
						{
							sb.Append(t.Repr);
							sb.Append(l.AssertNextTokenIs(Token.TokenType.CAHV_ATOM, Token.TokenType.CAHV_DOMAIN_LITERAL).Repr);
						} while (t.TType == Token.TokenType.CAHV_PERIOD);
					break;
				case Token.TokenType.CAHV_ROUTING_INFORMATION:
					sb.Append(t.Repr);
					break;
				case Token.TokenType.CAHV_COLON:
					do
					{
						foreach (String sEmailAddress in ParseAddress(l))
							addresses.Add(sEmailAddress);
					} while (l.PeekNextToken().TType != Token.TokenType.CAHV_SEMICOLON);
					sb.Clear();
					break;
			}
			if (sb.Length > 0)
				addresses.Add(sb.ToString());
			return addresses;
		} // end of ParseAddress

		ICollection<string> ParseAddressCommaSeparatedList(Lexer l)
		{
			var addrs = new List<string>();
			bool endOfList = false;
			while (!endOfList)
			{
				addrs.AddRange(ParseAddress(l));
				if (l.PeekNextToken(Lexer.ContextType.CONTEXT_ADDRESS_HEADER_VALUE).TType == Token.TokenType.CAHV_COMMA)
					l.NextToken(); // skip comma
				else
					endOfList = true;
			}
			return addrs;
		} // end of ParseAddressCommaSeparatedList

		MailAddress ParseAddressIntoMailAddress(Lexer l)
		{
			ICollection<String> sMailAddress = ParseAddress(l);
			if (sMailAddress.Count < 1)
				throw new ParserException(GetMessage("ParserError_CouldNotFindEmailAddressInMessageHeader"));
			return new MailAddress(sMailAddress.First<String>());
		} // end of ParseAddressIntoMailAddress

		MailAddressCollection ParseAddressesIntoMailAddressCollection(Lexer l)
		{
			ICollection<String> sMailAddresses = ParseAddress(l);
			if (sMailAddresses.Count < 1)
				throw new ParserException(GetMessage("ParserError_CouldNotFindEmailAddressInMessageHeader"));
			MailAddressCollection mac = new MailAddressCollection();
			foreach (String s in sMailAddresses)
				mac.Add(new MailAddress(s));
			return mac;
		} // end of ParseAddressesIntoMailAddressCollection

		string ParseUnstructuredTextWithEncodedWords(Lexer l)
		{
			StringBuilder sb = new StringBuilder();
			bool reachedEndOfText = false;
			while (!reachedEndOfText)
			{
				Token t = l.NextToken(Lexer.ContextType.CONTEXT_UNSTRUCTUREDTEXT_WITH_ENCODED_WORDS);
				if (t.TType != Token.TokenType.CUWEW_CRLF)
					sb.Append(t.Repr);
				else
					reachedEndOfText = true;
			}
			return sb.ToString();
		} // end of ParseUnstructuredTextWithEncodedWords

		string ParseContentType(Lexer l, out ParsedContentType pct)
		{
			//
			//	content := "Content-Type" ":" type "/" subtype *(";" parameter) ; Matching of media type and subtype is ALWAYS case-insensitive.
			//	type := "text" / "image" / "audio" / "video" / "application" / extension-token / "message" / "multipart" / extension-token
			//	parameter := token "=" (token / quoted-string)
			//	token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, or tspecials>
			//	tspecials :=  "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "=" ; Must be in quoted-string, to use within parameter values
			//
			//	comments are allowed
			//

			StringBuilder sb = new StringBuilder();
			string type = l.AssertNextTokenInContextIs(Lexer.ContextType.CONTEXT_CONTENT_HEADER_PARAM, Token.TokenType.CHP_TOKEN).Repr;
			sb.Append(type);
			sb.Append(l.AssertNextTokenIs(Token.TokenType.CHP_SLASH).Repr);
			string subtype = l.AssertNextTokenIs(Token.TokenType.CHP_TOKEN).Repr;
			sb.Append(subtype);
			Token t = l.PeekNextToken();
			var parameters = new Dictionary<string, IList<string>>();
			while (t.TType == Token.TokenType.CHP_SEMICOLON)
			{
				sb.Append(t.Repr);
				string paramname = l.AssertNextTokenIs(Token.TokenType.CHP_TOKEN).Repr;
				sb.Append(paramname);
				IList<string> paramvalues;
				if (!parameters.TryGetValue(paramname, out paramvalues))
				{
					paramvalues = new List<string>();
					parameters.Add(paramname, paramvalues);
				}
				sb.Append(l.AssertNextTokenIs(Token.TokenType.CHP_EQUAL).Repr);
				string paramvalue = l.AssertNextTokenIs(Token.TokenType.CHP_TOKEN, Token.TokenType.CHP_QUOTEDSTRING).Repr;
				paramvalues.Add(paramvalue);
				sb.Append(paramvalue);
			}
			l.AssertNextTokenIs(Token.TokenType.CHP_CRLF);
			pct = new ParsedContentType(type, subtype, parameters);
			return sb.ToString();
		} // end of ParseContentType

		string ParseContentDisposition(Lexer l, out ContentDisposition cd)
		{
			//
			//	from RFC 2183
			//
			//	disposition := "Content-Disposition" ":"
			//	               disposition-type
			//	               *(";" disposition-parm)
			//	
			//	disposition-type := "inline"
			//	                  / "attachment"
			//	                  / extension-token
			//	                  ; values are not case-sensitive
			//	
			//	disposition-parm := filename-parm
			//	                  / creation-date-parm
			//	                  / modification-date-parm
			//	                  / read-date-parm
			//	                  / size-parm
			//	                  / parameter
			//	
			//	filename-parm := "filename" "=" value
			//	
			//	creation-date-parm := "creation-date" "=" quoted-date-time
			//	
			//	modification-date-parm := "modification-date" "=" quoted-date-time
			//	
			//	read-date-parm := "read-date" "=" quoted-date-time
			//	
			//	size-parm := "size" "=" 1*DIGIT
			//	
			//	quoted-date-time := quoted-string	; contents MUST be an RFC 822 `date-time' numeric timezones (+HHMM or -HHMM) MUST be used
			//
			StringBuilder sb = new StringBuilder();
			string dispositiontype = l.AssertNextTokenInContextIs(Lexer.ContextType.CONTEXT_CONTENT_HEADER_PARAM, Token.TokenType.CHP_TOKEN).Repr;
			sb.Append(dispositiontype);
			Token t = l.PeekNextToken();
			cd = new ContentDisposition();
			cd.DispositionType = dispositiontype;
			cd.Inline = String.Compare(dispositiontype, "inline", true) == 0;
			while (t.TType == Token.TokenType.CHP_SEMICOLON)
			{
				sb.Append(t.Repr);
				string paramname = l.AssertNextTokenIs(Token.TokenType.CHP_TOKEN).Repr;
				sb.Append(paramname);
				sb.Append(l.AssertNextTokenIs(Token.TokenType.CHP_EQUAL).Repr);
				string paramvalue = l.AssertNextTokenIs(Token.TokenType.CHP_TOKEN, Token.TokenType.CHP_QUOTEDSTRING).Repr;
				sb.Append(paramvalue);
				switch (paramname.ToLower())
				{
					case "filename": cd.FileName = paramvalue; break;
					case "creation-date": cd.CreationDate = DateTime.Parse(paramvalue); break;
					case "modification-date": cd.ModificationDate = DateTime.Parse(paramvalue); break;
					case "read-date": cd.ReadDate = DateTime.Parse(paramvalue); break;
					case "size": cd.Size = long.Parse(paramvalue); break;
					default: cd.Parameters.Add(paramname, paramvalue); break;
				}
			}
			l.AssertNextTokenIs(Token.TokenType.CHP_CRLF);
			return sb.ToString();
		} // end of ParseContentDisposition

		class ParsedContentType
		{
			public string Type { get; set; }
			public string Subtype { get; set; }
			public bool IsMultipart { get { return String.Compare(Type, "multipart", true) == 0; } }
			Dictionary<string, IList<string>> parameters;
			public void AddParameter(string name, string value)
			{
				IList<string> paramvalues;
				if (!parameters.TryGetValue(name, out paramvalues))
				{
					paramvalues = new List<string>();
					parameters.Add(name, paramvalues);
				}
				paramvalues.Add(value);
			}
			public IList<string> GetParameterValues(string name) { return parameters[name]; }
			public string Boundary { get { return GetParameterValues("boundary").ElementAt<string>(0); } }

			public ParsedContentType(string type, string subtype, Dictionary<string, IList<string>> parameters) { Type = type; Subtype = subtype; this.parameters = parameters; }

			public string ToString()
			{
				StringBuilder sb = new StringBuilder(String.Format("{0}/{1}", Type, Subtype));
				foreach (KeyValuePair<string, IList<string>> kvp in parameters)
					sb.Append(String.Format(";{0}={1}", kvp.Key, kvp.Value));
				return sb.ToString();
			}
		} // end of ParsedContentType

		class GenericMailHeader
		{
			public string Name { get; set; }
			public ICollection<string> Value { get; set; }
			public string SingleValue { get { return (Value != null && Value.Count > 0? Value.ElementAt<string>(0): null); } }
			public GenericMailHeader(string name, string value) { Name = name; Value = new List<String>(); Value.Add(value); }
			public GenericMailHeader(string name, ICollection<string> value) { Name = name; Value = value; }
		}
		class MailHeader
		{
			ICollection<GenericMailHeader> headers;
			public ICollection<GenericMailHeader> Headers { get { return headers; } }
			public void AddHeader(GenericMailHeader gmh) { headers.Add(gmh); }
			public ParsedContentType PCT { get; set; }
			public TransferEncoding CTE { get; set; }
			public void SetCTEfromString(String s)
			{
				switch (s)
				{
					case "base64": CTE = TransferEncoding.Base64; break;
					case "quoted-printable": CTE = TransferEncoding.QuotedPrintable; break;
					case "7bit": CTE = TransferEncoding.SevenBit; break;
					default: CTE = TransferEncoding.Unknown; break;
				}
			}

			public ContentDisposition CD { get; set; }
			public string ContentId { get; set; }

			public MailHeader() { headers = new List<GenericMailHeader>(); }
		}

		private MailHeader ParseHeaders(Lexer l)
		{
			var mh = new MailHeader();
			//
			//	Message headers
			//
			bool reachedEndOfHeaders = false;
			while (!reachedEndOfHeaders)
			{
				Token t = l.NextToken(Lexer.ContextType.HEADER_FIELD);
				if (t.TType == Token.TokenType.CHN_HEADERNAME)
				{
					l.AssertNextTokenIs(Token.TokenType.CHN_COLON);

					string loweredheadername = t.Repr.ToLower();
					switch (loweredheadername)
					{
						case "from": 
						case "sender": mh.AddHeader(new GenericMailHeader(loweredheadername, ParseAddress(l).ElementAt<string>(0))); break;
						case "reply-to": 
						case "to": 
						case "cc":
						case "bcc": mh.AddHeader(new GenericMailHeader(loweredheadername, ParseAddressCommaSeparatedList(l))); break;
						case "subject":
						case "comments": mh.AddHeader(new GenericMailHeader(loweredheadername, ParseUnstructuredTextWithEncodedWords(l))); break;
						//
						//	from RFC 2045
						//
						case "mime-version":
							//
							//	parse text ignoring comments in parenthesis
							//
							t = l.AssertNextTokenInContextIs(Lexer.ContextType.CONTEXT_MIMEVERSION_HEADER_VALUE, Token.TokenType.CMHV_MIMEVERSION_VALUE);
							const string MIME_VERSION_EXPECTED_VALUE = "1.0";
							if (t.Repr.CompareTo(MIME_VERSION_EXPECTED_VALUE) == 0)
								throw new ParserException(GetMessage("ParserError_UnexpectedMIMEVersionValue", t.Repr, MIME_VERSION_EXPECTED_VALUE));
							break;
						case "content-type":
							ParsedContentType pct;
							mh.AddHeader(new GenericMailHeader(loweredheadername, ParseContentType(l, out pct)));
							mh.PCT = pct;
							break;
						case "content-transfer-encoding":
							//
							//	encoding := "Content-Transfer-Encoding" ":" mechanism
							//	
							//	mechanism := "7bit" / "8bit" / "binary" / "quoted-printable" / "base64" / ietf-token / x-token
							//
							t = l.AssertNextTokenInContextIs(Lexer.ContextType.CONTEXT_UNSTRUCTUREDTEXT, Token.TokenType.CU_TEXT);
							mh.SetCTEfromString(t.Repr);
							mh.AddHeader(new GenericMailHeader(loweredheadername, t.Repr));
							break;
						case "content-disposition":
							ContentDisposition cd;
							mh.AddHeader(new GenericMailHeader(loweredheadername, ParseContentDisposition(l, out cd)));
							mh.CD = cd;
							break;
						case "content-id":
						//
						//	id := "Content-ID" ":" msg-id
						//
							t = l.AssertNextTokenInContextIs(Lexer.ContextType.CONTEXT_UNSTRUCTUREDTEXT, Token.TokenType.CU_TEXT);
							mh.ContentId = t.Repr;
							mh.AddHeader(new GenericMailHeader(loweredheadername, mh.ContentId));
							break;
						case "content-description":
						//
						//	description := "Content-Description" ":" *text
						//
						case "date":
						case "resent-date":
						case "return-path":
						case "resent-from":
						case "resent-sender":
						case "resent-reply-to":
						case "resent-to":
						case "resent-cc":
						case "resent-bcc":
						default:
							mh.AddHeader(new GenericMailHeader(loweredheadername, l.AssertNextTokenInContextIs(Lexer.ContextType.CONTEXT_UNSTRUCTUREDTEXT, Token.TokenType.CU_TEXT).Repr));
							break;
					}
				}
				else if (t.TType == Token.TokenType.CHN_EMPTYLINE)
					reachedEndOfHeaders = true;
			}
			return mh;
		} // end of ParseHeaders

		class EmbeddedMessage
		{
			public MailHeader MH { get; private set; }

			public Stream S { get; private set; }

			public IList<EmbeddedMessage> EmbeddedMessages { get; private set; }
			public void AddEmbeddedMsg(EmbeddedMessage em) { EmbeddedMessages.Add(em); }

			public EmbeddedMessage(MailHeader mh, Stream s)
			{
				MH = mh;
				S = s;
				EmbeddedMessages = new List<EmbeddedMessage>();
			}
			public EmbeddedMessage(MailHeader mh, IList<EmbeddedMessage> ems)
			{
				MH = mh;
				EmbeddedMessages = ems;
			}
			public EmbeddedMessage(Stream s) { S = s; }
		}

		private IList<EmbeddedMessage> ParseBody(Lexer l, Stack<String> boundaries, String boundaryThisLevel = null)
		{
			if (boundaryThisLevel != null) // parsing multipart body
				boundaries.Push(boundaryThisLevel);
	
			IList<EmbeddedMessage> messages = new List<EmbeddedMessage>();
			//
			//	Message body
			//
			StringBuilder sbBody = new StringBuilder();
			bool reachedEndOfBody = false;
			string boundaryForeignActuallyFound = null;
			while (!reachedEndOfBody)
			{
				Token tBody = l.PeekNextToken(Lexer.ContextType.CONTEXT_MESSAGE_BODY);
				switch (tBody.TType)
				{
					case Token.TokenType.CMB_BOUNDARY:
						if (boundaryThisLevel == null || String.Compare(tBody.Repr, boundaryThisLevel) != 0)
						{
							//
							//	not our boundary - bail out right away - it could be our parents
							//
							boundaryForeignActuallyFound = tBody.Repr;
							reachedEndOfBody = true;
						}
						else
						{
							l.NextToken(); // it is our boundary, so consume the token
							//
							//	Parse message starting from this boundary down (till next boundary).
							//
							MailHeader mh = ParseHeaders(l);
							//
							//	Recursively called ParseBody is going to stop at next boundary it can't recognize(ours or our parents)
							//
							IList<EmbeddedMessage> msgs = ParseBody(l, boundaries, mh.PCT.IsMultipart ? mh.PCT.Boundary : null);
							if (mh.PCT.IsMultipart)
								messages.Add(new EmbeddedMessage(mh, msgs));
							else
								//
								//	when not a multipart message, then body is first (and only) message returned from ParseBody
								//
								messages.Add(new EmbeddedMessage(mh, msgs[0].S));
						}
						break;
					case Token.TokenType.CMB_FINAL_BOUNDARY:
						if (boundaryThisLevel == null || String.Compare(tBody.Repr, boundaryThisLevel) != 0)
						{
							//
							//	not our boundary - bail out right away - it could be our parents
							//
							boundaryForeignActuallyFound = tBody.Repr;
							reachedEndOfBody = true;
						}
						else
							l.NextToken(); // it is our boundary, so consume the token and keep going until we either hit parents boundary or run out of tokens in this message
						break;
					case Token.TokenType.CMB_EOF:
						reachedEndOfBody = true;
						break;
					default:
						l.NextToken();
						sbBody.Append(tBody.Repr);
						break;
				}
			}
			if (boundaryThisLevel != null)
				if (boundaryForeignActuallyFound == null) // gracefully terminated message
					boundaries.Pop();
				else { }
			else
			{
				//
				// if not a multipart body, then body is what we collected ourselves
				//
				MemoryStream ms = new MemoryStream();
				StreamWriter sw = new StreamWriter(ms, Encoding.ASCII);
				sw.Write(sbBody.ToString());
				sw.Flush();
				messages.Add(new EmbeddedMessage(ms));
			}
			return messages;
		} // end of ParseBody

		public MailMessage ExtractMailMessage(Stream s)
		{
			Lexer l = new Lexer(s, Lexer.ContextType.HEADER_FIELD);

			MailMessage mm = new MailMessage();
			MailHeader mh = ParseHeaders(l);
			//
			//	Populate designated MailMessage properties based on particular headers values
			//
			foreach (GenericMailHeader gmh in mh.Headers)
				switch (gmh.Name.ToLower())
				{
					case "from":	mm.From = new MailAddress(gmh.SingleValue); break;
					case "sender":	mm.Sender = new MailAddress(gmh.SingleValue); break;
					case "reply-to": foreach (string sReplyTo in gmh.Value) { mm.ReplyToList.Add(new MailAddress(sReplyTo)); } break;
					case "to":		foreach (string sTo in gmh.Value) { mm.To.Add(new MailAddress(sTo)); } break;
					case "cc":		foreach (string sCC in gmh.Value) { mm.CC.Add(new MailAddress(sCC)); } break;
					case "bcc":		foreach (string sBcc in gmh.Value) { mm.Bcc.Add(new MailAddress(sBcc)); } break;
					case "subject": mm.Subject = gmh.SingleValue; break;
					default:
						mm.Headers.Add(gmh.Name, gmh.SingleValue);
						break;
				}

			//
			//	After headers comes the body
			//
			var boundaries = new Stack<string>();
			IList<EmbeddedMessage> msgs;
			if (mh.PCT.IsMultipart)
			{
				boundaries.Push(mh.PCT.Boundary);
				msgs = ParseBody(l, boundaries, mh.PCT.Boundary);
			}
			else
				msgs = ParseBody(l, boundaries);

			if (mh.PCT.IsMultipart)
				//
				//	For multipart body each part goes into designated collection in MailMessage
				//
				switch (mh.PCT.Subtype)
				{
					case "alternative":
						foreach (EmbeddedMessage em in msgs)
						{
							AlternateView av = new AlternateView(em.S, new ContentType(em.MH.PCT.ToString()));
							av.TransferEncoding = em.MH.CTE;
							av.ContentId = em.MH.ContentId;
							mm.AlternateViews.Add(av);
						}
						break;
					case "digest":
					case "mixed":
					case "parallel":
					default:
						foreach (EmbeddedMessage em in msgs)
						{
							Attachment a = new Attachment(em.S, new ContentType(em.MH.PCT.ToString()));
							if (em.MH.CD != null)
							{
								a.ContentDisposition.Parameters.Clear();
								foreach (string key in em.MH.CD.Parameters.Keys)
									a.ContentDisposition.Parameters.Add(key, em.MH.CD.Parameters[key]);

								a.ContentDisposition.CreationDate = em.MH.CD.CreationDate;
								a.ContentDisposition.DispositionType = em.MH.CD.DispositionType;
								a.ContentDisposition.FileName = em.MH.CD.FileName;
								a.ContentDisposition.Inline = em.MH.CD.Inline;
								a.ContentDisposition.ModificationDate = em.MH.CD.ModificationDate;
								a.ContentDisposition.ReadDate = em.MH.CD.ReadDate;
								a.ContentDisposition.Size = em.MH.CD.Size;
							}
							a.ContentId = em.MH.ContentId;
							a.TransferEncoding = em.MH.CTE;

							mm.Attachments.Add(a);
						}
						break;
				}
			else
			{
				//
				//	For regular body main concern is just encoding of the contents
				//
				Stream bodystream = msgs.ElementAt<EmbeddedMessage>(0).S;
				switch (mh.PCT.Type)
				{
					case "message":
						switch (mh.PCT.Subtype)
						{
							case "partial":
							case "external-body":
							case "rfc822":
							default:
								break;
						}
						break;
					case "text":
						switch (mh.PCT.Subtype)
						{
							case "plain":
								string charset = null;
								var valsCharset = mh.PCT.GetParameterValues("charset");
								if (valsCharset != null && valsCharset.Count > 0)
									charset = valsCharset.ElementAt<string>(0);
								if (charset == null)
									charset = "us-ascii";
								mm.BodyEncoding = Encoding.GetEncoding(charset);
								StreamReader sr = new StreamReader(bodystream, mm.BodyEncoding);

								switch (mh.CTE)
								{
									case TransferEncoding.QuotedPrintable:
										Decoder decoder = mm.BodyEncoding.GetDecoder();

										StringBuilder sb = new StringBuilder();
										int lastpeq = 0;
										String bodystring = sr.ReadToEnd();
										int peq = bodystring.IndexOf('=');
										while (peq >= 0)
										{
											sb.Append(bodystring.Substring(lastpeq, peq - lastpeq + 1));

											byte code = Byte.Parse(bodystring.Substring(peq + 1, 2));
											char[] tchars = new char[1];
											decoder.GetChars(new byte[] { code }, 0, 1, tchars, 0, true);
											sb.Append(tchars[0]);

											lastpeq = peq;
											if (lastpeq < s.Length)
												peq = bodystring.IndexOf('=', lastpeq + 1);
											else
												peq = -1;
										}
										if (peq < s.Length - 1)
											sb.Append(bodystring.Substring(peq + 1));
										mm.Body = sb.ToString();
										break;
									case TransferEncoding.Base64:
										mm.Body = mm.BodyEncoding.GetString(Convert.FromBase64String(sr.ReadToEnd()));
										break;
									default:
										mm.Body = sr.ReadToEnd();
										break;
								}
								break;
						}
						break;
					case "image":
					case "audio":
					case "video":
						break;
					case "application":
						switch (mh.PCT.Subtype)
						{
							case "octet-stream":
							case "postscript":
								break;
						}
						break;
				}
			}
			return mm;
		} // end of ExtractMailMessage
	}
}
